跳到主要内容

Spring 中的事件机制

参考资料 Spring 中的事件机制 参考资料 深入理解Spring的容器内事件发布监听机制

观察者模式和事件监听机制

在讲解事件监听机制前,我们先回顾下设计模式中的观察者模式,因为事件监听机制可以说是在典型观察者模式基础上的进一步抽象和改进。我们可以在 JDK 或者各种开源框架比如 Spring 中看到它的身影,从这个意义上说,事件监听机制也可以看做一种对传统观察者模式的具体实现,不同的框架对其实现方式会有些许差别。下面先来看下典型的观察者模式是怎么样的

观察者模式 定义了一对多的依赖关系,让多个观察者对象同时监听某一个主题。这个主题在状态发生变化时,会通知所有观察者对象,使它们能够自动更新自己

class Subject {
constructor() {
this.observers = [];
}

add(observer) {
this.observers.push(observer);
}

notify(...args) {
this.observers.forEach(observer => observer.update(...args));
}
}

class Observer {
update(...args) {
console.log(...args);
}
}

// 创建观察者ob1
let ob1 =new Observer();
// 创建观察者ob2
let ob2 =new Observer();
// 创建目标sub
let sub =new Subject();
// 目标sub添加观察者ob1 (目标和观察者建立了依赖关系)
sub.add(ob1);
// 目标sub添加观察者ob2
sub.add(ob2);
// 目标sub触发SMS事件(目标主动通知观察者)
sub.notify('I fired `SMS` event');

典型的观察者模式将有依赖关系的对象抽象为了观察者和主题两个不同的角色,多个观察者同时观察一个主题,两者只通过抽象接口保持松耦合状态,这样双方可以相对独立的进行扩展和变化:比如可以很方便的增删观察者,修改观察者中的更新逻辑而不用修改主题中的代码。但是这种解耦进行的并不彻底,这具体体现在以下几个方面:

  1. 抽象主题需要依赖抽象观察者,而这种依赖关系完全可以去除。
  2. 主题需要维护观察者列表,并对外提供动态增删观察者的接口。
  3. 主题状态改变时需要由自己去通知观察者进行更新。

可以把主题(Subject)替换成事件(event),把对特定主题进行观察的观察者(Observer)替换成对特定事件进行监听的监听器(EventListener),而把原有主题中负责维护主题与观察者映射关系以及在自身状态改变时通知观察者的职责从中抽出,放入一个新的角色事件发布器(EventPublisher)中,事件监听模式的轮廓就展现在了我们眼前,如下图所示

常见事件监听机制的主要角色如下:

  • 事件及事件源:对应于观察者模式中的主题。事件源发生某事件是特定事件监听器被触发的原因。
  • 事件监听器:对应于观察者模式中的观察者。监听器监听特定事件,并在内部定义了事件发生后的响应逻辑。
  • 事件发布器:事件监听器的容器,对外提供发布事件和增删事件监听器的接口,维护事件和事件监听器之间的映射关系,并在事件发生时负责通知相关监听器。

使用 JavaScript 简单实现:

class EventChannel {
constructor() {
this.subscribers = [];
}

subscribe(topic, callback) {
let callbacks =this.subscribers[topic];
if(!callbacks) {
this.subscribers[topic] = [callback];
}else{
callbacks.push(callback);
}
}

publish(topic, ...args) {
letcallbacks =this.subscribers[topic] || [];
callbacks.forEach(callback => callback(...args));
}
}

// 创建事件调度中心,为订阅者和发布者提供调度服务
let eventChannel = new EventChannel();

// A订阅了SMS事件(A只关注SMS本身,而不关心谁发布这个事件)
eventChannel.subscribe('SMS', console.log);
// B订阅了SMS事件
eventChannel.subscribe('SMS', console.log);

// C发布了SMS事件(C只关注SMS本身,不关心谁订阅了这个事件)
eventChannel.publish('SMS','I published `SMS` event');

JDK中对事件监听机制的支持

Spring 框架对事件的发布与监听提供了相对完整的支持,它扩展了 JDK 中对自定义事件监听提供的基础框架,并与 Spring 的 IoC 特性作了整合,使得用户可以根据自己的业务特点进行相关的自定义,并依托 Spring 容器方便的实现监听器的注册和事件的发布。因为 Spring 的事件监听依托于 JDK 提供的底层支持,为了更好的理解,先来看下 JDK 中为用户实现自定义事件监听提供的基础框架。

JDK 为用户实现自定义事件监听提供了两个基础的类。一个是代表所有可被监听事件的事件基类 java.util.EventObject,所有自定义事件类型都必须继承该类,类结构如下所示

public class EventObject implements java.io.Serializable {

private static final long serialVersionUID = 5516075349620653480L;

/**
* The object on which the Event initially occurred.
*/
protected transient Object source;

/**
* Constructs a prototypical Event.
*
* @param source The object on which the Event initially occurred.
* @exception IllegalArgumentException if source is null.
*/
public EventObject(Object source) {
if (source == null)
throw new IllegalArgumentException("null source");

this.source = source;
}

/**
* The object on which the Event initially occurred.
*
* @return The object on which the Event initially occurred.
*/
public Object getSource() {
return source;
}

/**
* Returns a String representation of this EventObject.
*
* @return A a String representation of this EventObject.
*/
public String toString() {
return getClass().getName() + "[source=" + source + "]";
}
}

该类内部有一个 Object 类型的 source 变量,逻辑上表示发生该事件的事件源,实际中可以用来存储包含该事件的一些相关信息。

另一个则是对所有事件监听器进行抽象的接口 java.util.EventListener,这是一个标记接口,内部没有任何抽象方法,所有自定义事件监听器都必须实现该标记接口

/**
* A tagging interface that all event listener interfaces must extend.
* @since JDK1.1
*/
public interface EventListener {
}

基于JDK实现对任务执行结果的监听

想象我们正在做一个关于 Spark 的任务调度系统,我们需要把任务提交到集群中并监控任务的执行状态,当任务执行完毕(成功或者失败),除了必须对数据库进行更新外,还可以执行一些额外的工作:比如将任务执行结果以邮件的形式发送给用户。

这些额外的工作后期还有较大的变动可能:比如还需要以短信的形式通知用户,对于特定的失败任务需要通知相关运维人员进行排查等等,所以不宜直接写死在主流程代码中。

最好的方式自然是以事件监听的方式动态的增删对于任务执行结果的处理逻辑。为此我们可以基于 JDK 提供的事件框架,打造一个能够对任务执行结果进行监听的弹性系统。

任务结束事件的事件源

因为要对任务执行结束这一事件进行监听,所以必须对任务这一概念进行定义,如下:

@Data
public class Task {

private String name;

private TaskFinishStatus status;

}

任务包含任务名和任务状态,其中任务状态是个枚举常量,只有成功和失败两种取值。

public enum  TaskFinishStatus {
SUCCEED,
FAIL;
}

任务结束事件 TaskFinishEvent

自定义事件类型 TaskFinishEvent 继承自 JDK 中的 EventObject,构造时会传入 Task 作为事件源。

public class TaskFinishEvent extends EventObject {
/**
* Constructs a prototypical Event.
*
* @param source The object on which the Event initially occurred.
* @throws IllegalArgumentException if source is null.
*/
public TaskFinishEvent(Object source) {
super(source);
}
}

该事件的监听器抽象

继承标记接口 EventListener 表示该接口的实现类是一个监听器,同时在内部定义了事件发生时的响应方法 onTaskFinish(event),接收一个 TaskFinishEvent 作为参数。

public interface TaskFinishEventListener extends EventListener {

void onTaskFinish(TaskFinishEvent event);
}

邮件服务监听器

该邮件服务监听器将在监听到任务结束事件时将任务的执行结果发送给用户。

@Data
public class MailTaskFinishListener implements TaskFinishEventListener {

private String email;

@Override
public void onTaskFinish(TaskFinishEvent event) {
System.out.println("Send Email to " + email +" Task:" + event.getSource());
}
}

自定义事件发布器

public class TaskFinishEventPublisher {

private List<MailTaskFinishListener> listeners = new ArrayList<>();

//注册监听器
public synchronized void register(MailTaskFinishListener listener){
if(!listeners.contains(listener)){
listeners.add(listener);
}
}

//移除监听器
public synchronized boolean remove(MailTaskFinishListener listener){
return listeners.remove(listener);
}


//发布任务结束事件
public void publishEvent(TaskFinishEvent event){

for(MailTaskFinishListener listener : listeners){
listener.onTaskFinish(event);
}
}
}

测试代码

public class TestTaskFinishListener {


public static void main(String[] args) {

//事件源
Task source = new Task("用户统计", TaskFinishStatus.SUCCEED);

//任务结束事件
TaskFinishEvent event = new TaskFinishEvent(source);

//邮件服务监听器
MailTaskFinishListener mailListener = new MailTaskFinishListener("temp@163.com");

//事件发布器
TaskFinishEventPublisher publisher = new TaskFinishEventPublisher();

//注册邮件服务监听器
publisher.register(mailListener);

//发布事件
publisher.publishEvent(event);

}
}

如果后期因为需求变动需要在任务结束时将结果以短信的方式发送给用户,则可以再添加一个短信服务监听器

@Data
@AllArgsConstructor
public class SmsTaskFinishListener implements TaskFinishEventListener {

private String address;

@Override
public void onTaskFinish(TaskFinishEvent event) {
System.out.println("Send Message to "+ address+" Task:"+event.getSource());
}
}

在测试代码中添加如下代码向事件发布器注册该监听器

SmsTaskFinishListener smsListener = new SmsTaskFinishListener("123456789");

//注册短信服务监听器
publisher.register(smsListener);

Spring 容器对事件监听机制的支持

基于 JDK 的支持要实现对自定义事件的监听还是比较麻烦的,要做的工作比较多。而且自定义的事件发布器也不能提供对所有事件的统一发布支持。基于 Spring 框架实现自定义事件监听则要简单很多,功能也更加强大。

Spring 容器,具体而言是 ApplicationContext 接口定义的容器提供了一套相对完善的事件发布和监听框架,其遵循了 JDK 中的事件监听标准,并使用容器来管理相关组件,使得用户不用关心事件发布和监听的具体细节,降低了开发难度也简化了开发流程。

下面看看对于事件监听机制中的各主要角色,Spring框架中是如何定义的,以及相关的类体系结构

事件 ApplicationEvent

Spring 为容器内事件定义了一个抽象类 ApplicationEvent,该类继承了 JDK 中的事件基类 EventObject。因而自定义容器内事件除了需要继承 ApplicationEvent 之外,还要传入事件源作为构造参数。

Spring 中默认存在以下事件,他们都是对 ApplicationContextEvent 的实现(继承自 ApplicationContextEvent):

  • ContextStartedEvent:ApplicationContext 启动后触发的事件;
  • ContextStoppedEvent:ApplicationContext 停止后触发的事件;
  • ContextRefreshedEvent:ApplicationContext 初始化或刷新完成后触发的事件;
  • ContextClosedEvent:ApplicationContext 关闭后触发的事件。

事件监听器 ApplicationListener

Spring 定义了一个 ApplicationListener 接口作为为事件监听器的抽象,接口定义如下

@FunctionalInterface
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
void onApplicationEvent(E var1);
}

1、该接口继承了 JDK 中表示事件监听器的标记接口 EventListener,内部只定义了一个抽象方法 onApplicationEvent(event),当监听的事件在容器中被发布,该方法将被调用。

2、同时,该接口是一个泛型接口,其实现类可以通过传入泛型参数指定该事件监听器要对哪些事件进行监听。这样有什么好处?

这样所有的事件监听器就可以由一个事件发布器进行管理,并对所有事件进行统一发布,而具体的事件和事件监听器之间的映射关系,则可以通过反射读取泛型参数类型的方式进行匹配,稍后我们会对原理进行讲解。

3、最后,所有的事件监听器都必须向容器注册,容器能够对其进行识别并委托容器内真正的事件发布器进行管理。

事件发布器

ApplicationEventPublisher 充当了事件的发布者,它也是一个接口。

@FunctionalInterface
public interface ApplicationEventPublisher {
default void publishEvent(ApplicationEvent event) {
this.publishEvent((Object)event);
}

void publishEvent(Object var1);
}

ApplicationContext 接口继承了 ApplicationEventPublisher 接口,从而提供了对外发布事件的能力,如下所示

那么是否可以说 ApplicationContext,即容器本身就担当了事件发布器的角色呢?

其实这是不准确的,容器本身仅仅是对外提供了事件发布的接口,真正的工作其实是委托给了具体容器内部一个 ApplicationEventMulticaster 对象,其定义在 AbstractApplicationContext 抽象类内部,如下所示

/** Helper class used in event publishing */
private ApplicationEventMulticaster applicationEventMulticaster;

所以,真正的事件发布器是 ApplicationEventMulticaster,这是一个接口,定义了事件发布器需要具备的基本功能:管理事件监听器以及发布事件。其默认实现类是

SimpleApplicationEventMulticaster,该组件会在容器启动时被自动创建,并以单例的形式存在,管理了所有的事件监听器,并提供针对所有容器内事件的发布功能。

基于 Spring 对任务执行结果的监听

这里对上面使用 JDK 的例子进行重写

基于 Spring 框架来实现对自定义事件的监听流程十分简单,只需要三部:

  1. 自定义事件类
  2. 自定义事件监听器并向容器注册
  3. 发布事件

自定任务结束事件

定义一个任务结束事件 TaskFinishEvent2,该类继承抽象类 ApplicationEvent 来遵循容器事件规范。

public class TaskFinishEvent2 extends ApplicationEvent {
/**
* Create a new ApplicationEvent.
*
* @param source the object on which the event initially occurred (never {@code null})
*/
public TaskFinishEvent2(Object source) {
super(source);
}
}

自定义邮件服务监听器并向容器注册

该类实现了容器事件规范定义的监听器接口,通过泛型参数指定对上面定义的任务结束事件进行监听,通过 @Component 注解向容器进行注册

@Component
public class MailTaskFinishListener2 implements ApplicationListener<TaskFinishEvent2> {

private String email="temp@163.com";

@Override
public void onApplicationEvent(TaskFinishEvent2 event) {

System.out.println("Send Email to "+email+" Task:"+event.getSource());

}
}

发布事件

从上面对 Spring 事件监听机制的类结构分析可知,发布事件的功能定义在 ApplicationEventPublisher 接口中,而 ApplicationContext 继承了该接口,所以最好的方法是通过实现 ApplicationContextAware 接口获取 ApplicationContext 实例,然后调用其发布事件方法。如下所示定义了一个发布容器事件的代理类

@Component
public class EventPublisher implements ApplicationContextAware {

private ApplicationContext applicationContext;

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}

//发布事件
public void publishEvent(ApplicationEvent event){

applicationContext.publishEvent(event);
}


}

在此基础上,还可以自定义一个短信服务监听器,在任务执行结束时发送短信通知用户。过程和上面自定义邮件服务监听器类似:实现 ApplicationListener 接口并重写抽象方法,然后通过注解或者 xml 的方式向容器注册。

Spring 的事件流程总结

  1. 定义一个事件: 实现一个继承自 ApplicationEvent,并且写相应的构造函数;
  2. 定义一个事件监听者:实现 ApplicationListener 接口,重写 onApplicationEvent() 方法;
  3. 使用事件发布者发布消息: 可以通过 ApplicationEventPublisher 的 publishEvent() 方法发布消息。
// 定义一个事件,继承自ApplicationEvent并且写相应的构造函数
public class DemoEvent extends ApplicationEvent{
private static final long serialVersionUID = 1L;

private String message;

public DemoEvent(Object source,String message){
super(source);
this.message = message;
}

public String getMessage() {
return message;
}

}

// 定义一个事件监听者,实现ApplicationListener接口,重写 onApplicationEvent() 方法;
@Component
public class DemoListener implements ApplicationListener<DemoEvent>{

//使用onApplicationEvent接收消息
@Override
public void onApplicationEvent(DemoEvent event) {
String msg = event.getMessage();
System.out.println("接收到的信息是:"+msg);
}

}


// 发布事件,可以通过ApplicationEventPublisher 的 publishEvent() 方法发布消息。
@Component
public class DemoPublisher {

@Autowired
ApplicationContext applicationContext;

public void publish(String message){
//发布事件
applicationContext.publishEvent(new DemoEvent(this, message));
}
}

提供的 5种标准的事件

上下文更新事件(ContextRefreshedEvent):在调用 ConfigurableApplicationContext 接口中的 refresh() 方法时被触发。

上下文开始事件(ContextStartedEvent):当容器调用 ConfigurableApplicationContext 的 Start() 方法开始/重新开始容器时触发该事件。

下文停止事件(ContextStoppedEvent):当容器调用 ConfigurableApplicationContext 的 Stop() 方法停止容器时触发该事件。

上下文关闭事件(ContextClosedEvent):当 ApplicationContext 被关闭时触发该事件。容器被关闭时,其管理的所有单例 Bean 都被销毁。

请求处理事件(RequestHandledEvent):在 Web 应用中,当一个 http 请求(request)结束触发该事件。如果一个 bean 实现了 ApplicationListener 接口,当一个 ApplicationEvent 被发布以后,bean 会自动被通知。